from khronos.utils import Namespace

class Solution(object):
    """This class is meant as a standard container for solution data of any optimization problem. 
    It contains two attributes, which are the actual solution data and a metadata namespace, 
    containing information such as the objective function value, cpu time, and any other relevant 
    information."""
    def __init__(self, data, objective, cpu=None, **metadata):
        self.data = data
        self.metadata = Namespace(objective=objective, cpu=cpu, **metadata)
        
class SolutionList(list):
    """The SolutionList class provides an easy standard way to record solutions found by solvers 
    and plot a time series of the objective function value of the solutions. Also provides a 
    best() method to access the solution with lowest objective value in the list, as well as other 
    utility methods.""" 
    def copy(self):
        return self.__class__(self)
        
    def new(self, *args, **kwargs):
        """Creates a new Solution object and appends it to the list. The new object is returned 
        for convenience."""
        solution = Solution(*args, **kwargs)
        self.append(solution)
        return solution
        
    def clear(self):
        """Delete all items in the list."""
        del self[:]
        
    def best(self):
        """Returns the best solution in the list, or None if it's empty."""
        if len(self) > 0:
            return min(self, key=lambda s: s.metadata.objective)
        return None
        
    def plot(self, upper_bound_only=True, clip_cpu=None, filename=None, dpi=300):
        """Build and display (or save) a plot showing the solutions' objective value over time. 
        Note that this requires CPU time to be recorded. If filename is None, the plot is 
        displayed to the screen, otherwise it is saved to the specified file. The filename 
        extension will determine the format of the output file (e.g. pdf, eps, png, jpg)."""
        if len(self) == 0:
            raise ValueError("empty solution list")
        # Prepare the list of xs, ys, and min_ys
        solutions = sorted(self, key=lambda s: s.metadata.cpu)
        xs = [solutions[0].metadata.cpu]
        ys = [solutions[0].metadata.objective]
        min_ys = [solutions[0].metadata.objective]
        for sol in solutions[1:]:
            xs.extend([sol.metadata.cpu, sol.metadata.cpu])
            ys.extend([ys[-1], sol.metadata.objective])
            min_ys.extend([min_ys[-1], min(min_ys[-1], sol.metadata.objective)])
        if clip_cpu is not None:
            xs.append(clip_cpu)
            ys.append(ys[-1])
            min_ys.append(min_ys[-1])
        
        # Plot the data
        from matplotlib.pyplot import figure
        fig = figure()
        plot = fig.add_subplot(1, 1, 1)
        plot.set_xlabel("CPU time (s)")
        plot.set_ylabel("Objective")
        plot.plot(xs, min_ys, color="blue", linewidth=2, marker="o", markeredgecolor="blue")
        if not upper_bound_only:
            plot.plot(xs, ys, color="red", linewidth=1.5, linestyle="-.")
        if filename is None:
            fig.show()
        else:
            fig.savefig(filename, dpi=dpi)
        return fig
        